﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.IO;
using System.Reflection;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;

using FileCopy.Common;

namespace FileCopy.Receiver
{
    class Program
    {
        [STAThread] // Required to be able to access the clipboard.
        static void Main(string[] args)
        {
            DirectoryInfo baseFolder = new DirectoryInfo(Environment.CurrentDirectory);
            if (!baseFolder.Exists)
            {
                Console.WriteLine("Unable to find the working folder.");
                return;
            }

            // Initial messages showing the settings.
            Console.WriteLine(String.Format("Receive folder: {0}", baseFolder.FullName));
            Console.WriteLine();
            Console.WriteLine();

            // Wait for a file send command.
            Console.WriteLine("Waiting for a file.");
            do
            {
                string clipboardData = Clipboard.WaitForClipboardToChange(null, 0);
                if (Command.IsValid(clipboardData))
                {
                    // New file found. Send acknowledgement and start receiving.
                    Command.CommandObject command = Command.Get(clipboardData);
                    if (command.Type == CommandType.NewFile)
                    {
                        ReceiveFile(command, baseFolder.FullName);
                    }

                    Console.WriteLine();
                    Console.WriteLine();
                    Console.WriteLine("Waiting for a file.");
                }
                
                Thread.Sleep(100);
            }
            while (true);
        }

        /// <summary>
        /// Handles receiving a file.
        /// </summary>
        /// <param name="command">The command specifying a new file.</param>
        private static void ReceiveFile(Command.CommandObject fileCommand, string baseFolder)
        {
            // Send acknowledgement.
            Console.WriteLine(String.Format("Received new file request. File: {0}. Size: {1}", fileCommand.Key, fileCommand.Data));
            Clipboard.SetText(Command.GetAcknowledgement(fileCommand.Key, fileCommand.Type));

            // Create the file.
            FileInfo file = new FileInfo(Path.Combine(baseFolder, fileCommand.Key));
            long expectedLength = long.Parse(fileCommand.Data);

#if (DEBUG)
            Console.WriteLine(String.Format("  Creating file. File: {0}", fileCommand.Key));
#endif
            if (file.Directory == null || !file.Directory.Exists)
            {
                file.Directory.Create();
            }

            using (FileStream fileStream = file.Create())
            {
#if (DEBUG)
                Console.WriteLine(String.Format("  File created. File: {0}", fileCommand.Key));
#endif
                try
                {
                    // Receive the file contents until the end of file command is received.
                    string commandData = Clipboard.WaitForClipboardToChange(fileCommand.ToString(), 0);
                    string lastKeyAccepted = fileCommand.Key;
                    do
                    {
                        // Wait for the clipboard to change.
                        commandData = Clipboard.WaitForClipboardToChange(commandData, 0);

                        if (Command.IsValid(commandData))
                        {
                            Command.CommandObject command = Command.Get(commandData);
                            string acknowledgement;
                            switch (command.Type)
                            {
                                case CommandType.NewFile:
                                    if (String.Compare(command.Key, lastKeyAccepted) != 0)
                                    {
                                        throw new Exception("Unexpected new file command while already receiving a file.");
                                    }
                                    else
                                    {
                                        Console.WriteLine("New file command already received.");
                                    }

                                    lastKeyAccepted = command.Key;
                                    acknowledgement = Command.GetAcknowledgement(command.Key, CommandType.NewFile);
                                    Clipboard.SetText(acknowledgement);
                                    commandData = acknowledgement;
                                    break;

                                case CommandType.FileContent:
                                    bool corruptData = false;
                                    if (String.Compare(command.Key, lastKeyAccepted) != 0)
                                    {
                                        // Ensure the length is correct.
                                        int expectedDataHash = int.Parse(Regex.Match(command.Key, "(?<=\\:)\\-?\\d+").Value);
                                        string[] data = command.Data.Split(',');
                                        if (command.Data.GetHashCode() != expectedDataHash)
                                        {
                                            Console.WriteLine("Received data has incorrect hash. Expected: {0}. Actual: {1}", expectedDataHash, command.Data.GetHashCode());
                                            corruptData = true;
                                        }
                                        else
                                        {
                                            // Check array length.
                                            int expectedEndStreamPosition = int.Parse(Regex.Match(command.Key, "\\d+").Value);
                                            if (data.Length + fileStream.Position != expectedEndStreamPosition)
                                            {
                                                Console.WriteLine("Received data does not match the expected end stream position. Expected: {0}. Actual: {1}", expectedEndStreamPosition, data.Length + fileStream.Position);
                                                corruptData = true;
                                            }

                                            // Check everything can be converted to bytes.
                                            List<byte> bytes = null;
                                            try
                                            {
                                                bytes = data.Select(character => byte.Parse(character)).ToList();
                                            }
                                            catch (Exception)
                                            {
                                                Console.WriteLine("Received data is invalid.");
                                                corruptData = true;
                                            }

                                            if (!corruptData)
                                            {
                                                decimal progress = (Decimal)(fileStream.Length + data.Length) / (Decimal)expectedLength;
                                                Console.WriteLine(String.Format("Content received. Progress: {1}. Key: {2}. Data Length: {0}", command.Data.Length, progress.ToString("00.00%"), command.Key));
                                                bytes.ForEach(character => fileStream.WriteByte(character));
#if (DEBUG)
                                                Console.WriteLine("  Content saved to file.");
#endif
                                            }
                                        }
                                    }
                                    else
                                    {
                                        Console.WriteLine("File content command already received.");
                                    }

                                    if (!corruptData)
                                    {
                                        lastKeyAccepted = command.Key;
                                        acknowledgement = Command.GetAcknowledgement(command.Key, CommandType.FileContent);
                                    }
                                    else
                                    {
                                        acknowledgement = Command.Build(null, command.Key, CommandType.Resend).ToString();
                                    }
                                    Clipboard.SetText(acknowledgement);
                                    commandData = acknowledgement;

                                    break;

                                case CommandType.EndOfFile:
                                    if (String.Compare(command.Key, lastKeyAccepted) != 0)
                                    {
#if (DEBUG)
                                    Console.WriteLine("  End of file received.");
#endif
                                        fileStream.Close();

                                        int expectedHash = int.Parse(command.Data);
                                        int hash = Common.File.GetHash(file.FullName);

                                        if (hash != expectedHash)
                                        {
                                            Console.WriteLine("File received. Hash does not match. Hash: {0}. Expcted Hash: {1}", hash, expectedHash);
                                            file.Delete();
                                            Console.Read();
                                            return;
                                        }
                                    }
                                    else
                                    {
                                        Console.WriteLine("End of file command already received.");
                                    }

                                    lastKeyAccepted = command.Key;
                                    acknowledgement = Command.GetAcknowledgement(command.Key, CommandType.EndOfFile);
                                    Clipboard.SetText(acknowledgement);
                                    commandData = acknowledgement;

                                    Console.WriteLine("File received successfully. File: {0}", fileCommand.Key);
                                    
                                    return;
                            }
                        }
                    }
                    while (true);
                }
                catch (Exception)
                {
                    // Delete the file.
                    fileStream.Close();
                    file.Delete();
                    throw;
                }
            }
        }
    }
}
